package org.marketcetera.marketdata.core.webservice.impl; import java.net.ConnectException; import java.util.*; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import javax.annotation.concurrent.ThreadSafe; import javax.xml.ws.WebServiceException; import org.apache.commons.lang.Validate; import org.apache.commons.lang.builder.EqualsBuilder; import org.apache.commons.lang.builder.HashCodeBuilder; import org.marketcetera.core.ApplicationVersion; import org.marketcetera.core.Util; import org.marketcetera.core.notifications.ServerStatusListener; import org.marketcetera.core.publisher.ISubscriber; import org.marketcetera.core.publisher.PublisherEngine; import org.marketcetera.event.Event; import org.marketcetera.marketdata.Capability; import org.marketcetera.marketdata.Content; import org.marketcetera.marketdata.MarketDataRequest; import org.marketcetera.marketdata.core.Messages; import org.marketcetera.marketdata.core.manager.MarketDataProviderNotAvailable; import org.marketcetera.marketdata.core.manager.MarketDataRequestFailed; import org.marketcetera.marketdata.core.manager.MarketDataRequestTimedOut; import org.marketcetera.marketdata.core.manager.NoMarketDataProvidersAvailable; import org.marketcetera.marketdata.core.webservice.*; import org.marketcetera.trade.Instrument; import org.marketcetera.util.log.SLF4JLoggerProxy; import org.marketcetera.util.misc.ClassVersion; import org.marketcetera.util.ws.ContextClassProvider; import org.marketcetera.util.ws.stateful.Client; import org.marketcetera.util.ws.tags.AppId; import org.marketcetera.util.ws.wrappers.RemoteException; /* $License$ */ /** * Provides access to Market Data Nexus services. * * @author <a href="mailto:colin@marketcetera.com">Colin DuPlantis</a> * @version $Id: MarketDataServiceClientImpl.java 16901 2014-05-11 16:14:11Z colin $ * @since 2.4.0 */ @ThreadSafe @ClassVersion("$Id: MarketDataServiceClientImpl.java 16901 2014-05-11 16:14:11Z colin $") public class MarketDataServiceClientImpl implements MarketDataServiceClient { /* (non-Javadoc) * @see org.marketcetera.marketdata.core.webservice.MarketDataServiceClient#request(org.marketcetera.marketdata.MarketDataRequest, boolean) */ @Override public long request(MarketDataRequest inRequest, boolean inStreamEvents) { try { checkConnection(); return marketDataService.request(serviceClient.getContext(), inRequest, inStreamEvents); } catch (Exception e) { throw handleException(e); } } /* (non-Javadoc) * @see org.marketcetera.marketdata.core.webservice.MarketDataServiceClient#getLastUpdate(long) */ @Override public long getLastUpdate(long inRequestId) { try { checkConnection(); return marketDataService.getLastUpdate(serviceClient.getContext(), inRequestId); } catch (Exception e) { throw handleException(e); } } /* (non-Javadoc) * @see org.marketcetera.marketdata.core.webservice.MarketDataServiceClient#cancel(long) */ @Override public void cancel(long inRequestId) { try { checkConnection(); marketDataService.cancel(serviceClient.getContext(), inRequestId); } catch (Exception e) { throw handleException(e); } } /* (non-Javadoc) * @see org.marketcetera.marketdata.core.webservice.MarketDataWebServiceClient#getEvents(long) */ @Override public Deque<Event> getEvents(long inRequestId) { try { checkConnection(); return marketDataService.getEvents(serviceClient.getContext(), inRequestId); } catch (Exception e) { throw handleException(e); } } /* (non-Javadoc) * @see org.marketcetera.marketdata.core.webservice.MarketDataServiceClient#getAllEvents(java.util.List) */ @Override public Map<Long,LinkedList<Event>> getAllEvents(List<Long> inRequestIds) { try { checkConnection(); return marketDataService.getAllEvents(serviceClient.getContext(), inRequestIds); } catch (Exception e) { throw handleException(e); } } /* (non-Javadoc) * @see org.marketcetera.marketdata.core.webservice.MarketDataServiceClient#getSnapshot(org.marketcetera.trade.Instrument, org.marketcetera.marketdata.Content, java.lang.String) */ @Override public Deque<Event> getSnapshot(Instrument inInstrument, Content inContent, String inProvider) { try { checkConnection(); return marketDataService.getSnapshot(serviceClient.getContext(), inInstrument, inContent, inProvider); } catch (Exception e) { throw handleException(e); } } /* (non-Javadoc) * @see org.marketcetera.marketdata.core.webservice.MarketDataServiceClient#getSnapshotPage(org.marketcetera.trade.Instrument, org.marketcetera.marketdata.Content, java.lang.String, org.springframework.data.domain.PageRequest) */ @Override public Deque<Event> getSnapshotPage(Instrument inInstrument, Content inContent, String inProvider, PageRequest inPage) { try { checkConnection(); return marketDataService.getSnapshotPage(serviceClient.getContext(), inInstrument, inContent, inProvider, inPage); } catch (Exception e) { throw handleException(e); } } /* (non-Javadoc) * @see org.marketcetera.marketdata.core.webservice.MarketDataServiceClient#getAvailableCapability() */ @Override public Set<Capability> getAvailableCapability() { try { checkConnection(); return marketDataService.getAvailableCapability(serviceClient.getContext()); } catch (Exception e) { throw handleException(e); } } /* (non-Javadoc) * @see org.springframework.context.Lifecycle#isRunning() */ @Override public synchronized boolean isRunning() { return running.get(); } /* (non-Javadoc) * @see org.springframework.context.Lifecycle#start() */ @Override public synchronized void start() { Validate.notNull(hostname); Validate.notNull(username); Validate.notNull(password); Validate.isTrue(port > 0); serviceClient = new Client(hostname, port, APP_ID, contextClassProvider); try { serviceClient.login(username, password.toCharArray()); } catch (WebServiceException e) { if(e.getCause() != null && e.getCause() instanceof ConnectException) { throw new ConnectionException(hostname, port); } } catch (RemoteException e) { throw new CredentialsException(username); } catch (Exception e) { SLF4JLoggerProxy.warn(this, e); throw new RuntimeException(e); } marketDataService = serviceClient.getService(MarketDataService.class); // do one test heartbeat to catch a bad (lazy-loaded) connection exception here try { marketDataService.heartbeat(serviceClient.getContext()); } catch (RemoteException | WebServiceException e) { if(e.getCause() != null) { if(e.getCause() instanceof java.net.UnknownHostException) { throw new UnknownHostException(hostname, port); } } throw new RuntimeException(e); } heartbeatToken = heartbeatExecutor.scheduleAtFixedRate(new Runnable() { @Override public void run() { try { marketDataService.heartbeat(serviceClient.getContext()); } catch (Exception e) { heartbeatError(e); } } },heartbeatInterval,heartbeatInterval,TimeUnit.MILLISECONDS); running.set(true); } /* (non-Javadoc) * @see org.springframework.context.Lifecycle#stop() */ @Override public synchronized void stop() { try { if(heartbeatToken != null) { heartbeatToken.cancel(true); } } catch (Exception ignored) {} try { if(serviceClient != null) { serviceClient.logout(); } } catch (Exception ignored) { } finally { heartbeatToken = null; serviceClient = null; marketDataService = null; running.set(false); } } /* (non-Javadoc) * @see org.marketcetera.marketdata.core.webservice.MarketDataServiceClient#addServerStatusListener(org.marketcetera.core.notifications.ServerStatusListener) */ @Override public void addServerStatusListener(ServerStatusListener inListener) { serverStatusPublisher.subscribe(new ServerStatusSubscriber(inListener)); } /* (non-Javadoc) * @see org.marketcetera.marketdata.core.webservice.MarketDataServiceClient#removeServerStatusListener(org.marketcetera.core.notifications.ServerStatusListener) */ @Override public void removeServerStatusListener(ServerStatusListener inListener) { serverStatusPublisher.unsubscribe(new ServerStatusSubscriber(inListener)); } /* (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append("Market Data Service Client - ").append(hostname).append(":").append(port).append(" as ").append(username); return builder.toString(); } /** * Get the hostname value. * * @return a <code>String</code> value */ public String getHostname() { return hostname; } /** * Sets the hostname value. * * @param inHostname a <code>String</code> value */ public void setHostname(String inHostname) { hostname = inHostname; } /** * Get the port value. * * @return an <code>int</code> value */ public int getPort() { return port; } /** * Sets the port value. * * @param inPort an <code>int</code> value */ public void setPort(int inPort) { port = inPort; } /** * Get the contextClassProvider value. * * @return a <code>ContextClassProvider</code> value */ public ContextClassProvider getContextClassProvider() { return contextClassProvider; } /** * Sets the contextClassProvider value. * * @param inContextClassProvider a <code>ContextClassProvider</code> value */ public void setContextClassProvider(ContextClassProvider inContextClassProvider) { contextClassProvider = inContextClassProvider; } /** * Get the username value. * * @return a <code>String</code> value */ public String getUsername() { return username; } /** * Sets the username value. * * @param inUsername a <code>String</code> value */ public void setUsername(String inUsername) { username = inUsername; } /** * Sets the password value. * * @param inPassword a <code>String</code> value */ public void setPassword(String inPassword) { password = inPassword; } /** * Get the heartbeatInterval value. * * @return a <code>long</code> value */ public long getHeartbeatInterval() { return heartbeatInterval; } /** * Sets the heartbeatInterval value. * * @param inHeartbeatInterval a <code>long</code> value */ public void setHeartbeatInterval(long inHeartbeatInterval) { heartbeatInterval = inHeartbeatInterval; } /** * Rethrows an exception thrown by the remote connection and delivers the pertinent cause, if any. * * <p>It's not obvious from the method signature, but this method actually throws the exception, * not returns it. The signature is written this way to prevent a compilation error in the caller * if a method doesn't return a value after calling this method (which always throws an exception). * * @param inException an <code>Exception</code> value * @return a <code>RuntimeException</code> value */ private RuntimeException handleException(Exception inException) { if(inException.getCause() != null) { if(inException.getCause() instanceof NoMarketDataProvidersAvailable) { throw (NoMarketDataProvidersAvailable)inException.getCause(); } if(inException.getCause() instanceof MarketDataProviderNotAvailable) { throw (MarketDataProviderNotAvailable)inException.getCause(); } if(inException.getCause() instanceof MarketDataRequestFailed) { throw (MarketDataRequestFailed)inException.getCause(); } if(inException.getCause() instanceof MarketDataRequestTimedOut) { throw (MarketDataRequestTimedOut)inException.getCause(); } if(inException.getCause() instanceof ConnectionException) { throw (ConnectionException)inException.getCause(); } if(inException.getCause() instanceof WebServiceException) { throw (WebServiceException)inException.getCause(); } if(inException.getCause() instanceof ConnectException) { throw new ConnectionException(inException.getCause()); } if(inException.getCause() instanceof UnknownRequestException) { throw (UnknownRequestException)inException.getCause(); } } throw new RuntimeException(inException); } /** * Checks that the connection is up and running. * * @throws IllegalArgumentException if the connection is not running */ private void checkConnection() { Validate.isTrue(isRunning()); } /** * Indicates that an error occurred during a heartbeat request or response. * * <p>Attempts to make an orderly shutdown of the connection. * * @param inE an <code>Exception</code> value */ private void heartbeatError(Exception inE) { Messages.MARKETDATA_NEXUS_CONNECTION_LOST.error(this, inE); Messages.MARKETDATA_NEXUS_CONNECTION_LOST.error(org.marketcetera.core.Messages.USER_MSG_CATEGORY); reportServerStatus(false); try { stop(); } catch (Exception ignored) {} } /** * Processes updated status from the server. * * @param inUpdatedStatus a <code>boolean</code> value */ private void reportServerStatus(boolean inUpdatedStatus) { if(lastReportedStatus == null || lastReportedStatus != inUpdatedStatus) { serverStatusPublisher.publish(inUpdatedStatus); lastReportedStatus = inUpdatedStatus; } } /** * Subscribes to service status changes. * * @author <a href="mailto:colin@marketcetera.com">Colin DuPlantis</a> * @version $Id: MarketDataServiceClientImpl.java 16901 2014-05-11 16:14:11Z colin $ * @since 2.4.0 */ @ClassVersion("$Id: MarketDataServiceClientImpl.java 16901 2014-05-11 16:14:11Z colin $") private static class ServerStatusSubscriber implements ISubscriber { /* (non-Javadoc) * @see org.marketcetera.core.publisher.ISubscriber#isInteresting(java.lang.Object) */ @Override public boolean isInteresting(Object inData) { return true; } /* (non-Javadoc) * @see org.marketcetera.core.publisher.ISubscriber#publishTo(java.lang.Object) */ @Override public void publishTo(Object inData) { if(inData instanceof Boolean) { listener.receiveServerStatus((Boolean)inData); } else { throw new UnsupportedOperationException(); } } /* (non-Javadoc) * @see java.lang.Object#hashCode() */ @Override public int hashCode() { return new HashCodeBuilder().append(listener).toHashCode(); } /* (non-Javadoc) * @see java.lang.Object#equals(java.lang.Object) */ @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (!(obj instanceof ServerStatusSubscriber)) { return false; } ServerStatusSubscriber other = (ServerStatusSubscriber)obj; return new EqualsBuilder().append(other.listener,listener).isEquals(); } /** * Create a new ServerStatusSubscriber instance. * * @param inListener a <code>ServerStatusListener</code> value */ private ServerStatusSubscriber(ServerStatusListener inListener) { listener = inListener; } /** * listener value to which to announce server updates */ private final ServerStatusListener listener; } /** * tracks the last reported status value reported, may be <code>null</code>, indicating that status has not yet been reported */ private volatile Boolean lastReportedStatus = null; /** * tracks subscribers to and manages publications about server status changes */ private PublisherEngine serverStatusPublisher = new PublisherEngine(); /** * username with which to connect to the market data service */ private String username; /** * password with which to connect to the market data service */ private String password; /** * hostname at which to connect to the market data service */ private String hostname; /** * port at which to connect to the market data service */ private int port; /** * interval at which heartbeats are executed */ private long heartbeatInterval; /** * provides context classes for marshal/unmarshal */ private ContextClassProvider contextClassProvider; /** * market data service connection */ private MarketDataService marketDataService; /** * provides access to the service client */ private Client serviceClient; /** * represents the job responsible for sending and receiving heartbeats to the server */ private ScheduledFuture<?> heartbeatToken; /** * indicates if the client connection is up and running or not */ private final AtomicBoolean running = new AtomicBoolean(false); /** * application ID used to verify credentials with the market data nexus */ private static final AppId APP_ID = Util.getAppId("MDClient", ApplicationVersion.getVersion().getVersionInfo()); /** * executes heartbeat jobs */ private static final ScheduledExecutorService heartbeatExecutor = Executors.newScheduledThreadPool(1); }